package org.sanguine.study.hometasks.second;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;

import org.sanguine.study.hometasks.first.Runner;
import org.sanguine.study.hometasks.first.TaskWorker;
import org.sanguine.study.hometasks.second.auxiliary.Point;

public class Task15 extends Runner implements TaskWorker {
	
	class Label {
		Point parent = null;
	}
	
	protected PrintWriter out;
	protected Reader in;
	private String fileNameOfPoints;
	private File fileOfPoints;
	
	private Set<Point> setOfPoints = new HashSet<>();
	private List<Set<Point>> listOfFoundLines;
	private Map<Point, Label> stateGraph;
	
	public static void main(String[] args) {
		Runner runner = new Task15();
		runner.run(args);
	}
	
	@Override
	protected TaskWorker getWorkerInstance() {
		return this;
	}

	protected void printUsage() {
		System.err.println("USAGE: java -jar Task15.jar 'fileName_of_points' 'filename_output_list'");
	}

	@Override
	public boolean setUp(Reader in, PrintWriter out, String[] args) {
		if (args == null || args.length < 2) {
			printUsage();
			return false;
		}
		fileNameOfPoints = args[0];
		fileOfPoints = new File(fileNameOfPoints);
		if (fileOfPoints.isFile()) {
			try {
				this.in = new BufferedReader(new FileReader(fileOfPoints));
				this.out = new PrintWriter(new FileWriter(args[1]), true);
			} catch (IOException e) {
				e.printStackTrace();
				return false;
			}
			return true;
		}
		
		System.err.printf("File %s is not file or does not exist%n", fileNameOfPoints);
		fileOfPoints = null;
		return false;
	}

	@Override
	public void doTask() {
		if (fileOfPoints == null || in == null || out == null) return;
		readPoints();
		printLines();
	}
	
	private void readPoints() {
		Scanner scanner = new Scanner(in);
		scanner.useDelimiter("\\s+");
		while (scanner.hasNextDouble()) {
			Double x = scanner.nextDouble();
			if (scanner.hasNextDouble()) {
				Double y = scanner.nextDouble();
				addPoint(new Point(x, y));
			} else break;
		}
		scanner.close();
		try {
			in.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void addPoint(Point p) {
		if (p == null) throw new IllegalArgumentException("Argument must not be null");
		setOfPoints.add(p);
	}
	
	private void printLines() {
		List<Set<Point>> lines = getLinesOfThreePointsAndAbove();
		
		int i = 1;
		out.println("Found lines: " + lines.size());
		for (Collection<Point> line : lines) {
			out.printf("line %d (has %d points):", i++, line.size());
			for (Point p : line) out.print(" " + p);
			out.println();
		}
		out.close();
	}
	
	/**
	 * Find all lines containing three points and above for given point's set.
	 * @return List of found lines as set of its points
	 */

	private List<Set<Point>> getLinesOfThreePointsAndAbove() {  
		stateGraph = new HashMap<>(setOfPoints.size());
		listOfFoundLines = new ArrayList<Set<Point>>();
		for (Point point : setOfPoints) stateGraph.put(point, new Label());
		
		Iterator<Entry<Point, Label>> pointIterator = stateGraph.entrySet().iterator();
		while (pointIterator.hasNext()) {
			Entry<Point, Label> u = pointIterator.next();
			u.getValue().parent = null;
			dfsVisit(u);
		}
		stateGraph = null;
		return listOfFoundLines;
	}

	private void dfsVisit(Entry<Point, Label> u) {
		Iterator<Entry<Point, Label>> pointIterator = stateGraph.entrySet().iterator();
		while (pointIterator.hasNext()) {
			Entry<Point, Label> v = pointIterator.next();
			if (u.getKey().equals(v.getKey())) continue;
			if (u.getValue().parent == null) {
				v.getValue().parent = u.getKey();
				dfsVisit(v);
			}
			else if (u.getValue().parent.equals(v.getKey())) continue;
			else if (u.getValue().parent.isCollinear(u.getKey(), v.getKey())
						&& isNewSegment(u.getValue().parent, u.getKey(), v.getKey())) {
				addSegment(u.getValue().parent, u.getKey(), v.getKey());
				v.getValue().parent = u.getKey();
				dfsVisit(v);
			}
		}
	}
	
	private boolean isNewSegment(Point p0, Point p1, Point p2) {
		for (Set<Point> line : listOfFoundLines) 
			if (line.containsAll(Arrays.asList(p0, p1, p2))) return false;
		return true;
	}

	private void addSegment(Point p0, Point p1, Point p2) {
		boolean added = false;
		for (Set<Point> line : listOfFoundLines) {
			if (line.contains(p0) && line.contains(p1)) {
				line.addAll(Arrays.asList(p0, p1, p2));
				added = true;
			}
		}
		if (!added) listOfFoundLines.add(new HashSet<>(Arrays.asList(p0, p1, p2)));
	}
}
